#!/bin/bash

PROGRAM_FILE="$0"
PROGRAM_NAME="$(basename $0)"
PROGRAM_NAME="${PROGRAM_NAME/.plugin}"

# if you need to run parallel charts.d processes
# just link this files with a different name
# in the same directory, with a .plugin suffix
# netdata will start multiple of them
# each will have a different config file

echo >&2 "$PROGRAM_NAME: started from '$PROGRAM_FILE' with options: $*"

if [ $(( ${BASH_VERSINFO[0]} )) -lt 4 ]
then
	echo >&2
	echo >&2 "$PROGRAM_NAME: ERROR"
	echo >&2 "BASH version 4 or later is required."
	echo >&2 "You are running version: ${BASH_VERSION}"
	echo >&2 "Please upgrade."
	echo >&2
	exit 1
fi

# check a few commands
require_cmd() {
	which "$1" >/dev/null
	if [ $? -ne 0 ]
		then
		echo >&2 "$PROGRAM_NAME: ERROR: Command '$1' is not found in the system path."
		return 1
	fi
	return 0
}

require_cmd date || exit 1
require_cmd sed || exit 1
require_cmd basename || exit 1
require_cmd dirname || exit 1
require_cmd cat || exit 1
require_cmd grep || exit 1
require_cmd egrep || exit 1
require_cmd mktemp || exit 1
require_cmd awk || exit 1

# -----------------------------------------------------------------------------
# insternal defaults
# netdata exposes a few environment variables for us

pause_method="sleep" # use either "suspend" or "sleep"
                     # DO NOT USE SUSPEND - LINUX WILL SUSPEND NETDATA TOO
                     # THE WHOLE PROCESS GROUP - NOT JUST THE SHELL

pluginsd="${NETDATA_PLUGINS_DIR}"
[ -z "$pluginsd" ] && pluginsd="$( dirname $PROGRAM_FILE )"

confd="${NETDATA_CONFIG_DIR-/etc/netdata}"
chartsd="$pluginsd/../charts.d"

myconfig="$confd/$PROGRAM_NAME.conf"

minimum_update_frequency="${NETDATA_UPDATE_EVERY-1}"
update_every=${minimum_update_frequency}	# this will be overwritten by the command line

# work around for non BASH shells
charts_create="_create"
charts_update="_update"
charts_check="_check"
charts_undescore="_"

# when making iterations, charts.d can loop more frequently
# to prevent plugins missing iterations.
# this is a percentage relative to update_every to align its
# iterations.
# The minimum is 10%, the maximum 100%.
# So, if update_every is 1 second and time_divisor is 50,
# charts.d will iterate every 500ms.
# Charts will be called to collect data only if the time
# passed since the last time the collected data is equal or
# above their update_every.
time_divisor=50

# number of seconds to run without restart
# after this time, charts.d.plugin will exit
# netdata will restart it
restart_timeout=$((3600 * 4))

# check if the charts.d plugins are using global variables
# they should not.
# It does not currently support BASH v4 arrays, so it is
# disabled
dryrunner=0

# check for timeout command
check_for_timeout=1

# the default enable/disable value for all charts
enable_all_charts="yes"

# -----------------------------------------------------------------------------
# parse parameters

debug=0
check=0
chart_only=
while [ ! -z "$1" ]
do
	if [ "$1" = "check" ]
	then
		check=1
		shift
		continue
	fi

	if [ "$1" = "debug" -o "$1" = "all" ]
	then
		debug=1
		shift
		continue
	fi

	if [ -f "$chartsd/$1.chart.sh" ]
	then
		debug=1
		chart_only="$( echo $1.chart.sh | sed "s/\.chart\.sh$//g" )"
		shift
		continue
	fi

	if [ -f "$chartsd/$1" ]
	then
		debug=1
		chart_only="$( echo $1 | sed "s/\.chart\.sh$//g" )"
		shift
		continue
	fi

	# number check
	n="$1"
	x=$(( n ))
	if [ "$x" = "$n" ]
	then
		shift
		update_every=$x
		[ $update_every -lt $minimum_update_frequency ] && update_every=$minimum_update_frequency
		continue
	fi

	echo >&2 "Cannot understand parameter $1. Aborting."
	echo "DISABLE"
	exit 1
done


# -----------------------------------------------------------------------------
# load my configuration

if [ -f "$myconfig" ]
	then
	. "$myconfig"
	if [ $? -ne 0 ]
	then
		echo >&2 "$PROGRAM_NAME: cannot load $myconfig"
		echo "DISABLE"
		exit 1
	fi
	time_divisor=$((time_divisor))
	[ $time_divisor -lt 10 ] && time_divisor=10
	[ $time_divisor -gt 100 ] && time_divisor=100
else
	echo >&2 "$PROGRAM_NAME: configuration file '$myconfig' not found. Using defaults."
fi

if [ "$pause_method" = "suspend" ]
then
	# enable bash job control
	# this is required for suspend to work
	set -m
fi

# we check for the timeout command, after we load our
# configuration, so that the user may overwrite the
# timeout command we use, providing a function that
# can emulate the timeout command we need:
# > timeout SECONDS command ...
if [ $check_for_timeout -eq 1 ]
	then
	require_cmd timeout || exit 1
fi

# -----------------------------------------------------------------------------
# internal checks

# netdata passes the requested update frequency as the first argument
update_every=$(( update_every + 1 - 1))	# makes sure it is a number
test $update_every -eq 0 && update_every=1 # if it is zero, make it 1

# check the charts.d directory
if [ ! -d "$chartsd" ]
	then
	echo >&2 "$PROGRAM_NAME: cannot find charts directory '$chartsd'"
	echo "DISABLE"
fi


# -----------------------------------------------------------------------------
# loop control

# default sleep function
LOOPSLEEPMS_HIGHRES=0
loopsleepms() {
	[ "$1" = "tellwork" ] && shift
	sleep $1
}

now_ms=
current_time_ms() {
	now_ms="$(date +'%s')000"
}

# if found and included, this file overwrites loopsleepms()
# and current_time_ms() with a high resolution timer function
# for precise looping.
. "$pluginsd/loopsleepms.sh.inc"


# -----------------------------------------------------------------------------
# library functions

fixid() {
	echo "$*" |\
		tr -c "[A-Z][a-z][0-9]" "_" |\
		sed -e "s|^_\+||g" -e "s|_\+$||g" -e "s|_\+|_|g" |\
		tr "[A-Z]" "[a-z]"
}

# convert any floating point number
# to integer, give a multiplier
# the result is stored in ${FLOAT2INT_RESULT}
# so that no fork is necessary
# the multiplier must be a power of 10
float2int() {
	local f m="$2" a b l v=($1)
	f=${v[0]}

	# echo >&2 "value='${1}' f='${f}', m='${m}'"

	# the length of the multiplier - 1
	l=$(( ${#m} - 1 ))

	# check if the number is in scientific notation
	if [[ ${f} =~ ^[[:space:]]*(-)?[0-9.]+(e|E)(\+|-)[0-9]+ ]]
		then
		# convert it to decimal
		# unfortunately, this fork cannot be avoided
		# if you know of a way to avoid it, please let me know
		f=$(printf "%0.${l}f" ${f})
	fi

	# split the floating point number
	# in integer (a) and decimal (b)
	a=${f/.*/}
	b=${f/*./}

	# if the integer part is missing
	# set it to zero
	[ -z "${a}" ] && a="0"

	# strip leading zeros from the integer part
	# base 10 convertion
	a=$((10#$a))

	# check the length of the decimal part
	# against the length of the multiplier
	if [ ${#b} -gt ${l} ]
		then
		# too many digits - take the most significant
		b=${b:0:${l}}

	elif [ ${#b} -lt ${l} ]
		then
		# too few digits - pad with zero on the right
		local z="00000000000000000000000" r=$((l - ${#b}))
		b="${b}${z:0:${r}}"
	fi

	# strip leading zeros from the decimal part
	# base 10 convertion
	b=$((10#$b))

	# store the result
	FLOAT2INT_RESULT=$(( (a * m) + b ))
	#echo >&2 "FLOAT2INT_RESULT='${FLOAT2INT_RESULT}'"
}


# -----------------------------------------------------------------------------
# charts check functions

all_charts() {
	cd "$chartsd"
	[ $? -ne 0 ] && echo >&2 "$PROGRAM_NAME: Cannot cd to $chartsd" && return 1

	ls *.chart.sh | sed "s/\.chart\.sh$//g"
}

all_enabled_charts() {
	local charts= enabled=

	# find all enabled charts

	for chart in $( all_charts )
	do
		eval "enabled=\$$chart"
		if [ -z "${enabled}" ]
			then
			enabled="${enable_all_charts}"
		fi

		if [ ! "${enabled}" = "yes" ]
		then
			echo >&2 "$PROGRAM_NAME: '$chart' is NOT enabled. Add a line with $chart=yes in $myconfig to enable it (or remove the line that disables it)."
		else
			[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' is enabled."
			local charts="$charts $chart"
		fi
	done

	local charts2=
	for chart in $charts
	do
		# check the enabled charts
		local check="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_check()" )"
		if [ -z "$check" ]
		then
			echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_check() function. Disabling it."
			continue
		fi

		local create="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_create()" )"
		if [ -z "$create" ]
		then
			echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_create() function. Disabling it."
			continue
		fi

		local update="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_update()" )"
		if [ -z "$update" ]
		then
			echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_update() function. Disabling it."
			continue
		fi

		# check its config
		#if [ -f "$confd/$chart.conf" ]
		#then
		#	if [ ! -z "$( cat "$confd/$chart.conf" | sed "s/^ \+//g" | grep -v "^$" | grep -v "^#" | grep -v "^$chart$charts_undescore" )" ]
		#	then
		#		echo >&2 "$PROGRAM_NAME: chart's $chart config $confd/$chart.conf should only have lines starting with $chart$charts_undescore . Disabling it."
		#		continue
		#	fi
		#fi

		#if [ $dryrunner -eq 1 ]
		#	then
		#	"$pluginsd/charts.d.dryrun-helper.sh" "$chart" "$chartsd/$chart.chart.sh" "$confd/$chart.conf" >/dev/null
		#	if [ $? -ne 0 ]
		#	then
		#		echo >&2 "$PROGRAM_NAME: chart's $chart did not pass the dry run check. This means it uses global variables not starting with $chart. Disabling it."
		#		continue
		#	fi
		#fi

		local charts2="$charts2 $chart"
	done

	echo $charts2
	[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: enabled charts: $charts2"
}


# -----------------------------------------------------------------------------
# load the charts

suffix_update_every="_update_every"
active_charts=
for chart in $( all_enabled_charts )
do
	[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart: '$chartsd/$chart.chart.sh'"
	. "$chartsd/$chart.chart.sh"

	if [ -f "$confd/$chart.conf" ]
	then
		[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart options: '$confd/$chart.conf'"
		. "$confd/$chart.conf"
	else
		echo >&2 "$PROGRAM_NAME: $chart: configuration file '$confd/$chart.conf' not found. Using defaults."
	fi

	eval "dt=\$$chart$suffix_update_every"
	dt=$(( dt + 1 - 1 )) # make sure it is a number
	if [ $dt -lt $update_every ]
	then
		eval "$chart$suffix_update_every=$update_every"
	fi

	$chart$charts_check
	if [ $? -eq 0 ]
	then
		[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' activated"
		active_charts="$active_charts $chart"
	else
		echo >&2 "$PROGRAM_NAME: chart '$chart' check() function reports failure."
	fi
done
[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: activated charts: $active_charts"


# -----------------------------------------------------------------------------
# check overwrites

# enable work time reporting
debug_time=
test $debug -eq 1 && debug_time=tellwork

# if we only need a specific chart, remove all the others
if [ ! -z "${chart_only}" ]
then
	[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: requested to run only for: '${chart_only}'"
	check_charts=
	for chart in $active_charts
	do
		if [ "$chart" = "$chart_only" ]
		then
			check_charts="$chart"
			break
		fi
	done
	active_charts="$check_charts"
fi
[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: activated charts: $active_charts"

# stop if we just need a pre-check
if [ $check -eq 1 ]
then
	echo >&2 "CHECK RESULT"
	echo >&2 "Will run the charts: $active_charts"
	exit 0
fi

# -----------------------------------------------------------------------------
# create temp dir

TMP_DIR=
chartsd_cleanup() {
	cd /tmp
	if [ ! -z "$TMP_DIR" -a -d "$TMP_DIR" ]
	then
		[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: cleaning up temporary directory $TMP_DIR ..."
		rm -rf "$TMP_DIR"
	fi
	exit 0
}
trap chartsd_cleanup EXIT
trap chartsd_cleanup SIGHUP
trap chartsd_cleanup INT

if [ $UID = "0" ]
then
	TMP_DIR="$( mktemp -d /var/run/netdata-${PROGRAM_NAME}-XXXXXXXXXX )"
else
	TMP_DIR="$( mktemp -d /tmp/.netdata-${PROGRAM_NAME}-XXXXXXXXXX )"
fi

cd "$TMP_DIR" || exit 1

# -----------------------------------------------------------------------------
# create charts

run_charts=
for chart in $active_charts
do
	[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: Calling '$chart$charts_create()'..."
	$chart$charts_create
	if [ $? -eq 0 ]
	then
		run_charts="$run_charts $chart"
		[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' has initialized."
	else
		echo >&2 "$PROGRAM_NAME: chart '$chart' function '$chart$charts_create()' reports failure."
	fi
done
[ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: run_charts='$run_charts'"


# -----------------------------------------------------------------------------
# update dimensions

if [ -z "$run_charts" ]
	then
	echo >&2 "$PROGRAM_NAME: No charts to collect data from."
	echo "DISABLE"
	exit 1
fi

declare -A charts_last_update=() charts_update_every=() charts_next_update=() charts_run_counter=()
global_update() {
	local exit_at \
		c=0 dt ret last_ms exec_start_ms exec_end_ms \
		chart now_charts=() next_charts=($run_charts)

	# return the current time in ms in $now_ms
	current_time_ms

	exit_at=$(( now_ms + (restart_timeout * 1000) ))

	for chart in $run_charts
	do
		eval "charts_update_every[$chart]=\$$chart$suffix_update_every"
		test -z "${charts_update_every[$chart]}" && charts_update_every[$charts]=$update_every
		charts_last_update[$chart]=$((now_ms - (now_ms % (charts_update_every[$chart] * 1000) ) ))
		charts_next_update[$chart]=$(( charts_last_update[$chart] + (charts_update_every[$chart] * 1000) ))
		charts_run_counter[$chart]=0

		echo "CHART netdata.plugin_chartsd_$chart '' 'Execution time for $chart plugin' 'milliseconds / run' charts.d netdata.plugin_charts area 145000 ${charts_update_every[$chart]}"
		echo "DIMENSION run_time 'run time' absolute 1 1"
	done

	# the main loop
	while [ 1 ]
	do
		c=$((c + 1))
		now_charts=("${next_charts[@]}")
		next_charts=()

		# return the current time in ms in $now_ms
		current_time_ms

		for chart in "${now_charts[@]}"
		do
			# echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}"
			if [ ${now_ms} -ge ${charts_next_update[$chart]} ]
			then
				last_ms=${charts_last_update[$chart]}
				dt=$(( (now_ms - last_ms) ))
				# echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}, dt: ${dt}"

				charts_last_update[$chart]=${now_ms}

				while [ ${charts_next_update[$chart]} -lt ${now_ms} ]
				do
					charts_next_update[$chart]=$(( charts_next_update[$chart] + (charts_update_every[$chart] * 1000) ))
				done

				# the first call should not give a duration
				# so that netdata calibrates to current time
				dt=$(( dt * 1000 ))
				charts_run_counter[$chart]=$(( charts_run_counter[$chart] + 1 ))
				if [ ${charts_run_counter[$chart]} -eq 1 ]
					then
					dt=
				fi

				exec_start_ms=$now_ms
				$chart$charts_update $dt
				ret=$?
				
				# return the current time in ms in $now_ms
				current_time_ms; exec_end_ms=$now_ms

				echo "BEGIN netdata.plugin_chartsd_$chart $dt"
				if [ $ret -eq 0 ]
				then
					echo "SET run_time = $(( exec_end_ms - exec_start_ms ))"
					next_charts+=($chart)
				else
					echo "SET run_time = $(( (exec_end_ms - exec_start_ms) * -1 ))"
					echo >&2 "$PROGRAM_NAME: chart '$chart' update() function reports failure. Disabling it."
				fi
				echo "END"
			else
				next_charts+=($chart)
			fi
		done

		if [ "$pause_method" = "suspend" ]
		then
			echo "STOPPING_WAKE_ME_UP_PLEASE"
			suspend || ( echo >&2 "$PROGRAM_NAME: suspend returned error $?, falling back to sleep."; loopsleepms $debug_time $update_every $time_divisor)
		else
			# wait the time you are required to
			#loopsleepms $debug_time $update_every $time_divisor
			if [ ${LOOPSLEEPMS_HIGHRES} -eq 1 ]
				then
				sleep 0.2
			else
				sleep 1
			fi
		fi

		test ${now_ms} -ge ${exit_at} && exit 0
	done
}

global_update
